/* * Sun Public License Notice * * The contents of this file are subject to the Sun Public License * Version 1.0 (the "License"). You may not use this file except in * compliance with the License. A copy of the License is available at * http://www.sun.com/ * * The Original Code is Forte for Java, Community Edition. The Initial * Developer of the Original Code is Sun Microsystems, Inc. Portions * Copyright 1997-2000 Sun Microsystems, Inc. All Rights Reserved. */ package org.netbeans.editor; import java.awt.Rectangle; import java.awt.Graphics; import java.awt.Color; import java.awt.Font; import java.awt.Point; import java.awt.Component; import java.awt.event.MouseListener; import java.awt.event.MouseEvent; import java.awt.event.MouseMotionListener; import java.awt.event.ActionListener; import java.awt.event.ActionEvent; import java.awt.event.FocusListener; import java.awt.event.FocusEvent; import java.awt.event.InputEvent; import java.lang.ref.WeakReference; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeEvent; import javax.swing.Action; import javax.swing.Timer; import javax.swing.SwingUtilities; import javax.swing.text.BadLocationException; import javax.swing.text.Document; import javax.swing.text.JTextComponent; import javax.swing.text.Caret; import javax.swing.event.ChangeListener; import javax.swing.event.ChangeEvent; import javax.swing.event.DocumentListener; import javax.swing.event.DocumentEvent; import javax.swing.event.EventListenerList; /** * Caret implementation * * @author Miloslav Metelka * @version 1.00 */ public class BaseCaret extends Rectangle implements Caret, FocusListener, MouseListener, MouseMotionListener, PropertyChangeListener, DocumentListener, ActionListener, SettingsChangeListener { /** Caret type representing block covering current character */ public static final String BLOCK_CARET = "block-caret"; // NOI18N /** Default caret type */ public static final String LINE_CARET = "line-caret"; // NOI18N /** One dot thin line compatible with Swing default caret */ public static final String THIN_LINE_CARET = "thin-line-caret"; // NOI18N private static final boolean debugCaretFocus = Boolean.getBoolean("netbeans.debug.editor.caret.focus"); // NOI18N /** Component this caret is bound to */ protected JTextComponent component; /** Position of the caret on the screen. This helps to compute * caret position on the next after jump. */ Point magicCaretPosition; /** Draw mark designating the position of the caret. */ MarkFactory.DrawMark caretMark = new MarkFactory.CaretMark(); /** Draw mark that supports caret mark in creating selection */ MarkFactory.DrawMark selectionMark = new MarkFactory.DrawMark( DrawLayerFactory.CARET_LAYER_NAME, null); /** Is the caret visible */ boolean visible; /** Caret is visible and the blink is visible. Both must be true * in order to show the caret. */ boolean blinkVisible; /** Is the selection currently visible? */ boolean selectionVisible; /** Listeners */ protected EventListenerList listenerList = new EventListenerList(); /** Timer used for blinking the caret */ protected Timer flasher; /** Type of the caret */ String type; /** Is the caret italic for italic fonts */ boolean italic; private int xPoints[] = new int[4]; private int yPoints[] = new int[4]; private Action selectWordAction; private Action selectLineAction; /** Change event. Only one instance needed because it has only source property */ protected ChangeEvent changeEvent; private static char emptyDotChar[] = { ' ' }; /** Dot array of one character under caret */ protected char dotChar[] = emptyDotChar; private boolean overwriteMode; /** Remembering document on which caret listens avoids * duplicate listener addition to SwingPropertyChangeSupport * due to the bug 4200280 */ private BaseDocument listenDoc; /** Caret draw graphics */ CaretDG caretDG = new CaretDG(); /** Font of the text underlying the caret. It can be used * in caret painting. */ protected Font textFont; /** Font of the text right before the caret */ protected Font previousFont; /** Foreground color of the text underlying the caret. It can be used * in caret painting. */ protected Color textForeColor; /** Background color of the text underlying the caret. It can be used * in caret painting. */ protected Color textBackColor; private PropertyChangeListener settingsListener; private transient FocusListener focusListener; static final long serialVersionUID =-9113841520331402768L; public BaseCaret() { Settings.addSettingsChangeListener(this); } /** Called when settings were changed. The method is called * also in constructor, so the code must count with the evt being null. */ public void settingsChange(SettingsChangeEvent evt) { updateType(); } void updateType() { JTextComponent c = component; if (c != null) { Class kitClass = Utilities.getKitClass(c); String newType; boolean newItalic; Color caretColor; if (overwriteMode) { newType = SettingsUtil.getString(kitClass, Settings.CARET_TYPE_OVERWRITE_MODE, LINE_CARET); newItalic = SettingsUtil.getBoolean(kitClass, Settings.CARET_ITALIC_OVERWRITE_MODE, false); } else { // insert mode newType = SettingsUtil.getString(kitClass, Settings.CARET_TYPE_INSERT_MODE, LINE_CARET); newItalic = SettingsUtil.getBoolean(kitClass, Settings.CARET_ITALIC_INSERT_MODE, false); } this.type = newType; this.italic = newItalic; dispatchUpdate(); } } /** Called when UI is being installed into JTextComponent */ public void install(JTextComponent c) { component = c; component.addPropertyChangeListener(this); focusListener = new FocusHandler(this); component.addFocusListener(focusListener); component.addMouseListener(this); component.addMouseMotionListener(this); ExtUI extUI = Utilities.getExtUI(component); extUI.addLayer(new DrawLayerFactory.CaretLayer()); caretMark.setExtUI(extUI); selectionMark.setExtUI(extUI); BaseDocument doc = Utilities.getDocument(c); if (doc != null) { modelChanged(null, doc); } if (component.hasFocus()) { focusGained(null); // emulate focus gained } } /** Called when UI is being removed from JTextComponent */ public void deinstall(JTextComponent c) { component = null; // invalidate if (flasher != null) { setBlinkRate(0); } Utilities.getExtUI(c).removeLayer(DrawLayerFactory.CARET_LAYER_NAME); c.removeMouseMotionListener(this); c.removeMouseListener(this); if (focusListener != null) { c.removeFocusListener(focusListener); focusListener = null; } c.removePropertyChangeListener(this); modelChanged(listenDoc, null); } protected void modelChanged(BaseDocument oldDoc, BaseDocument newDoc) { // [PENDING] !!! this body looks strange because of the bug 4200280 if (oldDoc != null && listenDoc == oldDoc) { oldDoc.removeDocumentListener(this); try { caretMark.remove(); selectionMark.remove(); } catch (InvalidMarkException e) { if (Boolean.getBoolean("netbeans.debug.exceptions")) { // NOI18N e.printStackTrace(); } } listenDoc = null; } if (newDoc != null) { settingsChange(null); // update settings if (listenDoc != null) { // deinstall from the listenDoc first modelChanged(listenDoc, null); } newDoc.addDocumentListener(this); listenDoc = newDoc; try { Utilities.insertMark(newDoc, caretMark, 0); Utilities.insertMark(newDoc, selectionMark, 0); } catch (InvalidMarkException e) { if (Boolean.getBoolean("netbeans.debug.exceptions")) { // NOI18N e.printStackTrace(); } } catch (BadLocationException e) { if (Boolean.getBoolean("netbeans.debug.exceptions")) { // NOI18N e.printStackTrace(); } } SwingUtilities.invokeLater( new Runnable() { public void run() { updateType(); } } ); } } /** Renders the caret */ public void paint(Graphics g) { if (visible && blinkVisible) { paintCustomCaret(g); } } protected void paintCustomCaret(Graphics g) { JTextComponent c = component; if (c != null) { // && textFont != null) { ExtUI extUI = Utilities.getExtUI(c); if (THIN_LINE_CARET.equals(type)) { // thin line caret g.setColor(c.getCaretColor()); int upperX = x; if (previousFont != null && previousFont.isItalic() && italic) { upperX += Math.tan(previousFont.getItalicAngle()) * height; } g.drawLine((int)upperX, y, x, (y + height - 1)); } else if (BLOCK_CARET.equals(type)) { // block caret g.setColor(c.getCaretColor()); g.setFont(textFont); if (textFont.isItalic() && italic) { int upperX = (int)(x + Math.tan(textFont.getItalicAngle()) * height); xPoints[0] = upperX; yPoints[0] = y; xPoints[1] = upperX + width; yPoints[1] = y; xPoints[2] = x + width; yPoints[2] = y + height - 1; xPoints[3] = x; yPoints[3] = y + height - 1; g.fillPolygon(xPoints, yPoints, 4); } else { g.fillRect(x, y, width, height); } g.setColor(Color.white); g.drawChars(dotChar, 0, 1, x, y + extUI.ascents[0]); } else { // two dot line caret g.setColor(c.getCaretColor()); int blkWidth = 2; if (previousFont != null && previousFont.isItalic() && italic) { int upperX = (int)(x + Math.tan(previousFont.getItalicAngle()) * height); xPoints[0] = upperX; yPoints[0] = y; xPoints[1] = upperX + blkWidth; yPoints[1] = y; xPoints[2] = x + blkWidth; yPoints[2] = y + height - 1; xPoints[3] = x; yPoints[3] = y + height - 1; g.fillPolygon(xPoints, yPoints, 4); } else { g.fillRect(x, y, blkWidth, height - 1); } } } } /** Update the caret's visual position */ void dispatchUpdate() { dispatchUpdate(null, ExtUI.SCROLL_MOVE); } void dispatchUpdate(final Rectangle scrollRect, final int scrollPolicy) { Utilities.runInEventDispatchThread( new Runnable() { public void run() { update(scrollRect, scrollPolicy); } } ); } protected void update(Rectangle scrollRect, int scrollPolicy) { JTextComponent c = component; if (c != null) { BaseTextUI ui = (BaseTextUI)c.getUI(); ExtUI extUI = ui.getExtUI(); BaseDocument doc = Utilities.getDocument(c); if (doc != null) { if (scrollRect == null) { scrollRect = this; } doc.readLock(); try { Rectangle oldCaretRect = new Rectangle(this); int dot = getDot(); try { ui.modelToViewDG(dot, caretDG); } catch (BadLocationException e) { // Sometimes thrown at document closing // !!! if (Boolean.getBoolean("netbeans.debug.exceptions")) { // NOI18N // !!! e.printStackTrace(); // !!! } } resetBlink(); if (!extUI.scrollRectToVisibleFragile(scrollRect, scrollPolicy)) { oldCaretRect.add(this); c.repaint(oldCaretRect); } } finally { doc.readUnlock(); } } } } /** Redefine to Object.equals() to prevent defaulting to Rectangle.equals() * which would cause incorrect firing */ public boolean equals(Object o) { return (this == o); } /** Adds listener to track when caret position was changed */ public void addChangeListener(ChangeListener l) { listenerList.add(ChangeListener.class, l); } /** Removes listeners to caret position changes */ public void removeChangeListener(ChangeListener l) { listenerList.remove(ChangeListener.class, l); } /** Notifies listeners that caret position has changed */ protected void fireStateChanged() { Object listeners[] = listenerList.getListenerList(); for (int i = listeners.length - 2; i >= 0 ; i -= 2) { if (listeners[i] == ChangeListener.class) { if (changeEvent == null) { changeEvent = new ChangeEvent(this); } ((ChangeListener)listeners[i + 1]).stateChanged(changeEvent); } } } /** Is the caret currently visible */ public final boolean isVisible() { return visible; } protected void setVisibleImpl(boolean v) { Timer t = flasher; if (t != null) { if (visible) { t.stop(); } if (v) { t.start(); } else { t.stop(); } } visible = v; JTextComponent c = component; if (c != null) { c.repaint(this); } } void resetBlink() { Timer t = flasher; if (t != null) { t.stop(); blinkVisible = true; t.start(); } } /** Sets the caret visibility */ public void setVisible(final boolean v) { SwingUtilities.invokeLater( new Runnable() { public void run() { setVisibleImpl(v); } } ); } /** Is the selection visible? */ public final boolean isSelectionVisible() { return selectionVisible; } /** Sets the selection visibility */ public void setSelectionVisible(boolean v) { if (selectionVisible == v) { return; } JTextComponent c = component; if (c != null) { selectionVisible = v; if (selectionVisible) { int caretPos = getDot(); int selPos = getMark(); boolean selMarkFirst = (selPos < caretPos); selectionMark.activateLayer = selMarkFirst; caretMark.activateLayer = !selMarkFirst && !(selPos == caretPos); } else { // make selection invisible caretMark.activateLayer = false; selectionMark.activateLayer = false; } // repaint the block BaseTextUI ui = (BaseTextUI)c.getUI(); try { ui.getExtUI().repaintBlock(caretMark.getOffset(), selectionMark.getOffset()); } catch (BadLocationException e) { if (Boolean.getBoolean("netbeans.debug.exceptions")) { // NOI18N e.printStackTrace(); } } catch (InvalidMarkException e) { if (Boolean.getBoolean("netbeans.debug.exceptions")) { // NOI18N e.printStackTrace(); } } } } /** Saves the current caret position. This is used when * caret up or down actions occur, moving between lines * that have uneven end positions. * * @param p the Point to use for the saved position */ public void setMagicCaretPosition(Point p) { magicCaretPosition = p; } /** Get position used to mark begining of the selected block */ public final Point getMagicCaretPosition() { return magicCaretPosition; } /** Sets the caret blink rate. * @param rate blink rate in milliseconds, 0 means no blink */ public synchronized void setBlinkRate(int rate) { if (flasher == null && rate > 0) { flasher = new Timer(rate, this); } if (flasher != null) { if (rate > 0) { if (flasher.getDelay() != rate) { flasher.setDelay(rate); } } else { // zero rate - don't blink flasher.stop(); flasher.removeActionListener(this); flasher = null; } } } /** Returns blink rate of the caret or 0 if caret doesn't blink */ public synchronized int getBlinkRate() { return (flasher != null) ? flasher.getDelay() : 0; } /** Gets the current position of the caret */ public int getDot() { if (component != null) { try { return caretMark.getOffset(); } catch (InvalidMarkException e) { } } return 0; } /** Gets the current position of the selection mark. * If there's a selection this position will be different * from the caret position. */ public int getMark() { if (component != null) { if (selectionVisible) { try { return selectionMark.getOffset(); } catch (InvalidMarkException e) { } } else { // selection not visible return getDot(); // must return same position as dot } } return 0; } public void setDot(int pos) { setDot(pos, null, ExtUI.SCROLL_DEFAULT); } /** Sets the caret position to some position. This * causes removal of the active selection. */ public void setDot(int pos, Rectangle scrollRect, int scrollPolicy) { JTextComponent c = component; if (c != null) { setSelectionVisible(false); BaseDocument doc = (BaseDocument)c.getDocument(); if (doc != null) { try { Utilities.moveMark(doc, caretMark, pos); } catch (BadLocationException e) { // setting the caret to wrong position leaves it at current position } catch (InvalidMarkException e) { if (Boolean.getBoolean("netbeans.debug.exceptions")) { // NOI18N e.printStackTrace(); } } } fireStateChanged(); dispatchUpdate(scrollRect, scrollPolicy); } } public void moveDot(int pos) { moveDot(pos, null, ExtUI.SCROLL_MOVE); } /** Makes selection by moving dot but leaving mark */ public void moveDot(int pos, Rectangle scrollRect, int scrollPolicy) { JTextComponent c = component; if (c != null) { BaseDocument doc = (BaseDocument)c.getDocument(); try { int oldCaretPos = getDot(); if (pos == oldCaretPos) { // no change return; } int selPos; // current position of selection mark if (selectionVisible) { selPos = selectionMark.getOffset(); } else { Utilities.moveMark(doc, selectionMark, oldCaretPos); selPos = oldCaretPos; } Utilities.moveMark(doc, caretMark, pos); if (selectionVisible) { // selection already visible boolean selMarkFirst = (selPos < pos); selectionMark.activateLayer = selMarkFirst; caretMark.activateLayer = !selMarkFirst && !(selPos == pos); Utilities.getExtUI(c).repaintBlock(oldCaretPos, pos); } else { // selection not yet visible setSelectionVisible(true); } } catch (BadLocationException e) { // position is incorrect } catch (InvalidMarkException e) { if (Boolean.getBoolean("netbeans.debug.exceptions")) { // NOI18N e.printStackTrace(); } } fireStateChanged(); dispatchUpdate(scrollRect, scrollPolicy); } } // DocumentListener methods public void insertUpdate(DocumentEvent evt) { JTextComponent c = component; if (c != null) { BaseDocument doc = (BaseDocument)component.getDocument(); BaseDocumentEvent bevt = (BaseDocumentEvent)evt; if ((bevt.isInUndo() || bevt.isInRedo()) && component == Utilities.getLastActiveComponent() ) { // in undo mode and current component setDot(evt.getOffset() + evt.getLength()); } else { fireStateChanged(); if (evt.getLength() == 0) { updateType(); setVisible(false); setVisible(c.isEnabled()); } dispatchUpdate(); } } } public void removeUpdate(DocumentEvent evt) { JTextComponent c = component; if (c != null) { BaseDocument doc = (BaseDocument)c.getDocument(); // make selection invisible if removal shrinked block to zero size if (selectionVisible && (getDot() == getMark())) { setSelectionVisible(false); } BaseDocumentEvent bevt = (BaseDocumentEvent)evt; if ((bevt.isInUndo() || bevt.isInRedo()) && c == Utilities.getLastActiveComponent() ) { // in undo mode and current component setDot(evt.getOffset()); } else { fireStateChanged(); dispatchUpdate(); } } } public void changedUpdate(DocumentEvent evt) { dispatchUpdate(); } // FocusListener methods public void focusGained(FocusEvent evt) { if (debugCaretFocus) { System.out.println("BaseCaret.focusGained() in doc=" + component.getDocument().getProperty(Document.TitleProperty)); } JTextComponent c = component; if (c != null) { updateType(); setVisible(c.isEnabled()); // invisible caret if disabled } } public void focusLost(FocusEvent evt) { if (debugCaretFocus) { System.out.println("BaseCaret.focusLost() in doc=" + component.getDocument().getProperty(Document.TitleProperty)); } setVisible(false); } // MouseListener methods public void mouseClicked(MouseEvent evt) { JTextComponent c = component; if (c != null) { if (SwingUtilities.isLeftMouseButton(evt)) { if (evt.getClickCount() == 2) { if (selectWordAction == null) { BaseTextUI ui = (BaseTextUI)c.getUI(); selectWordAction = ((BaseKit)ui.getEditorKit( c)).getActionByName(BaseKit.selectWordAction); } selectWordAction.actionPerformed(null); } else if (evt.getClickCount() == 3) { if (selectLineAction == null) { BaseTextUI ui = (BaseTextUI)c.getUI(); selectLineAction = ((BaseKit)ui.getEditorKit( c)).getActionByName(BaseKit.selectLineAction); } selectLineAction.actionPerformed(null); } } } } public void mousePressed(MouseEvent evt) { JTextComponent c = component; if (c != null) { // Position the cursor at the appropriate place in the document if (SwingUtilities.isLeftMouseButton(evt) || !isSelectionVisible()) { int pos = ((BaseTextUI)c.getUI()).viewToModel(c, evt.getX(), evt.getY()); if (pos >= 0) { if ((evt.getModifiers() & InputEvent.SHIFT_MASK) != 0) { moveDot(pos); } else { setDot(pos); } setMagicCaretPosition(null); } if (c.isEnabled()) { c.requestFocus(); } } // Show popup menu for right click if (SwingUtilities.isRightMouseButton(evt)) { Utilities.getExtUI(c).showPopupMenu(evt.getX(), evt.getY()); } } } public void mouseReleased(MouseEvent evt) { } public void mouseEntered(MouseEvent evt) { } public void mouseExited(MouseEvent evt) { } // MouseMotionListener methods public void mouseDragged(MouseEvent evt) { JTextComponent c = component; if (SwingUtilities.isLeftMouseButton(evt)) { if (c != null) { int pos = ((BaseTextUI)c.getUI()).viewToModel(c, evt.getX(), evt.getY()); moveDot(pos); } } } public void mouseMoved(MouseEvent evt) { } // PropertyChangeListener methods public void propertyChange(PropertyChangeEvent evt) { String propName = evt.getPropertyName(); if ("document".equals(propName)) { BaseDocument oldDoc = (evt.getOldValue() instanceof BaseDocument) ? (BaseDocument)evt.getOldValue() : null; BaseDocument newDoc = (evt.getNewValue() instanceof BaseDocument) ? (BaseDocument)evt.getNewValue() : null; modelChanged(oldDoc, newDoc); } else if (ExtUI.OVERWRITE_MODE_PROPERTY.equals(propName)) { Boolean b = (Boolean)evt.getNewValue(); overwriteMode = (b != null) ? b.booleanValue() : false; updateType(); } } // ActionListener methods /** Fired when blink timer fires */ public void actionPerformed(ActionEvent evt) { JTextComponent c = component; if (c != null) { blinkVisible = !blinkVisible; c.repaint(this); } } /** Caret draw graphics used to update the caret position * and the character the caret sits on. */ final class CaretDG extends Drawer.AbstractDG { public boolean targetPosReached(int pos, char ch, int x, int y, int charWidth, DrawContext ctx, Font previousFont) { JTextComponent c = BaseCaret.this.component; if (c != null) { BaseCaret.this.x = x; BaseCaret.this.y = y; BaseCaret.this.width = charWidth; BaseCaret.this.height = Utilities.getExtUI(c).charHeight; BaseCaret.this.textFont = ctx.getFont(); BaseCaret.this.previousFont = previousFont; BaseCaret.this.textForeColor = ctx.getForeColor(); BaseCaret.this.textBackColor = ctx.getBackColor(); BaseCaret.this.dotChar[0] = ch; } return false; } } private static class FocusHandler implements FocusListener { private transient FocusListener fl; FocusHandler(FocusListener fl) { this.fl = fl; } public void focusGained(FocusEvent e) { fl.focusGained(e); } public void focusLost(FocusEvent e) { fl.focusLost(e); } } } /* * Log * 49 Gandalf-post-FCS1.46.1.1 4/3/00 Miloslav Metelka undo update * 48 Gandalf-post-FCS1.46.1.0 3/8/00 Miloslav Metelka * 47 Gandalf 1.46 1/18/00 Miloslav Metelka * 46 Gandalf 1.45 1/16/00 Miloslav Metelka * 45 Gandalf 1.44 1/13/00 Miloslav Metelka * 44 Gandalf 1.43 1/10/00 Miloslav Metelka * 43 Gandalf 1.42 1/7/00 Miloslav Metelka * 42 Gandalf 1.41 1/4/00 Miloslav Metelka * 41 Gandalf 1.40 12/28/99 Miloslav Metelka * 40 Gandalf 1.39 11/27/99 Patrik Knakal * 39 Gandalf 1.38 11/14/99 Miloslav Metelka * 38 Gandalf 1.37 11/11/99 Miloslav Metelka * 37 Gandalf 1.36 11/8/99 Miloslav Metelka * 36 Gandalf 1.35 10/23/99 Ian Formanek NO SEMANTIC CHANGE - Sun * Microsystems Copyright in File Comment * 35 Gandalf 1.34 10/10/99 Miloslav Metelka * 34 Gandalf 1.33 10/8/99 Miloslav Metelka Stability improvement * 33 Gandalf 1.32 10/4/99 Miloslav Metelka * 32 Gandalf 1.31 9/15/99 Miloslav Metelka * 31 Gandalf 1.30 8/17/99 Miloslav Metelka * 30 Gandalf 1.29 8/9/99 Miloslav Metelka flasher resets synced * 29 Gandalf 1.28 7/29/99 Miloslav Metelka * 28 Gandalf 1.27 7/22/99 Miloslav Metelka * 27 Gandalf 1.26 7/20/99 Miloslav Metelka * 26 Gandalf 1.25 7/9/99 Miloslav Metelka * 25 Gandalf 1.24 7/2/99 Miloslav Metelka * 24 Gandalf 1.23 6/29/99 Miloslav Metelka Scrolling and patches * 23 Gandalf 1.22 6/25/99 Miloslav Metelka from floats back to ints * 22 Gandalf 1.21 6/22/99 Miloslav Metelka * 21 Gandalf 1.20 6/8/99 Miloslav Metelka * 20 Gandalf 1.19 5/18/99 Miloslav Metelka getDot() fix * 19 Gandalf 1.18 5/16/99 Miloslav Metelka * 18 Gandalf 1.17 5/15/99 Miloslav Metelka fixes * 17 Gandalf 1.16 5/13/99 Miloslav Metelka * 16 Gandalf 1.15 5/7/99 Miloslav Metelka line numbering and fixes * 15 Gandalf 1.14 5/5/99 Miloslav Metelka * 14 Gandalf 1.13 4/23/99 Miloslav Metelka Undo added and internal * improvements * 13 Gandalf 1.12 4/9/99 Miloslav Metelka * 12 Gandalf 1.11 4/8/99 Miloslav Metelka * 11 Gandalf 1.10 4/1/99 Miloslav Metelka * 10 Gandalf 1.9 3/30/99 Miloslav Metelka * 9 Gandalf 1.8 3/27/99 Miloslav Metelka * 8 Gandalf 1.7 3/23/99 Miloslav Metelka * 7 Gandalf 1.6 3/19/99 Miloslav Metelka * 6 Gandalf 1.5 3/18/99 Miloslav Metelka * 5 Gandalf 1.4 3/18/99 Miloslav Metelka * 4 Gandalf 1.3 3/18/99 Miloslav Metelka * 3 Gandalf 1.2 2/13/99 Miloslav Metelka * 2 Gandalf 1.1 2/9/99 Miloslav Metelka * 1 Gandalf 1.0 2/3/99 Miloslav Metelka * $ */